package org.NooLab.docserver.main;

import java.io.File;
import java.util.ArrayList;
import java.util.Observable;
import java.util.Observer;
import java.util.Map;
import java.util.Iterator;
import java.util.TreeMap;
import java.io.FileNotFoundException ;

import org.NooLab.utilities.files.DFutils;
import org.NooLab.utilities.logging.PrintLog;
import org.NooLab.utilities.strings.StringsUtil;

import org.NooLab.docobserver.structures.DocObservation;
import org.NooLab.docobserver.structures.DocumentObserverIntf;
import org.NooLab.docserver.component.DocoConstraintChecker;
import org.NooLab.docserver.component.WatchedFile;
import org.NooLab.docserver.properties.ControlSettingsSection;
import org.NooLab.docserver.properties.DocoControlProperties;
import org.NooLab.docserver.properties.DocumentObserverProperties;
import org.NooLab.docserver.storage.DocoDataBase;
import org.NooLab.docserver.util.XmlStringHandling;
import org.NooLab.itexx.storage.DbFieldRequests;

/*
 * The DocumentObserver supervises directories for changes on the file level.
 * 
 * The application uses a simple database for persistence.
 * This table does NOT contain the individual files, but only the folders url
 * 
 * 
 * 
 * 
 * 
 * add capability for detecting <rename> move, delete -> removing observers, adding observers
 *  
 * TODO   - removing folders.... adding folders on the fly
 *        - name patterns for folders   , like "_a"  "_a* or "*abc.*"  inclusion / exclusion
 *        - ClipboardOwner co;
 * 	      -
 * 
 *        - consolidating events from observer processes through data base, for offering it to external applications
 *          -->> forwarding the message via simple TCP 
 *          
 *        - limiting the size of the DB: by record count (500'000), by age of entries (10 days), whatever first
 *          note, that we record just the changes here!
 *                
 *        - limiting the number of processes: partitioning the set of supervised folders 
 *          requires also a invocation counter for any of the processes (dependent on number of partitions)
 *          
 *          
 *        
 * ClipboardOwnder method
   public void lostOwnership(Clipboard clipboard, Transferable data) {}
 * public class NodeCopyAction extends AbstractAction implements ClipboardOwner
 * 
 * 
 * add basic activities: 
 *   - move, 
 *   - copy (to other directories), 
 *   - rename (same directory), 
 *         this includes an automatic detection for moves := target name is in different dir 
 *   - log
 *   - dedicated observer process for directories
 *   - dedicated process to observe the configuration file, update of config without restart
 *   
 *   
 *   these activities are bound to an watcher ID, there could be multiples of those
 * 
 * TODO: supervise the whole tree of sub-directories...
 *       by defining all sub directories explicitly 
 *       -> we need instances of processes for each subdirectory 
 *       -> list of multiple directories, as multiple files, as found in this tree
 * TODO: use java class "FileChangeListener"
 * 
 * 
 * This class prepares the database and the observations according to the configuration file;
 * 
 * it also serves as Java Observer for the observation processes
 * 
 * 
 */


class DocumentObserver extends Observable implements Observer, Runnable,DocumentObserverIntf {

	String version = "v2.0" ; 
	String changehistory ="" ;

	protected DocumentObserverFactory docoFactory;
	protected DocumentObserverProperties docoProperties;
	DocoDataBase docoDataBase; 
	DocoConstraintChecker docoConstraintChecker;
	
	protected ObserverMsgHandling observerMsgHandling;
	
	int count;
	int delay = 100;
	int startup ;
	boolean includeSubDirectories = false;
	
	int consoleMsg=0; 
	boolean isActive=false; 
	
	public boolean changed =false;

    Thread docoThrd;

    Object change=null;
	
	
	// Observation contains name, last serial ID , Guid 
    Map<Integer, DocObservation> observationMap = new TreeMap<Integer, DocObservation>();
	ArrayList<String> observedDir = new ArrayList<String>();

	ArrayList<DocumentObserverFileCore> docoProcesses = new ArrayList<DocumentObserverFileCore>(); 
	DocoControlProperties docoControlProperties;	
	int maxProcessCount = -1;
	int startedObservations=0;
	boolean enginesStarted = false;
	
	StringsUtil strgutil = new StringsUtil();
	protected DFutils fileutil = new DFutils();
	protected PrintLog out ;
	DocumentObserverApplicationEventIntf appCallbackInstance = null;
	boolean tuttibene=true;
	
	// ========================================================================
	public DocumentObserver(DocumentObserverFactory factory) {
	
		docoFactory = factory;

		out = docoFactory.getOut();
		out.setPrefix("[Doco]") ;

		
		docoProperties = docoFactory.getDocoProperties() ;
	
		docoDataBase = docoFactory.getDocoDataBase() ;
		
		observerMsgHandling = new ObserverMsgHandling(this);
		docoConstraintChecker = new DocoConstraintChecker(docoProperties);
		
		
		count = 0;
		
		isActive = false ;
		docoThrd = new Thread(this, "docoThrd.main") ;
		
		// this.addObserver( docoFactory.getAppObserver() ); 
	}
	// ========================================================================

	
	@Override
	public void stop() {
		out.printErr(1, "stopping processes... please wait...");
		
		stopObservations() ;
		observerMsgHandling.stop();
		docoDataBase.close();
		
		out.printErr(1, "application has been stopped.");
	}

	@Override
	public void start() throws Exception {
		// 
		boolean dbOk;

		docoDataBase = docoFactory.getDocoDataBase() ;
		
		if (docoDataBase==null){
			throw(new Exception("database object has not been instantiated yet!")) ;
		}

		
		dbOk = docoDataBase.prepareDatabase();
		
		if (dbOk==false){
			// message
			return;
		}
		
		appCallbackInstance = docoFactory.appInstance ;
		
		out.print(2, "retrieving folders according to the configuration...") ;
		prepareObservations();
		
		docoThrd.start() ;
		
	}

	public void run() {
		isActive = true;
		
		observerMsgHandling.start();
		startedObservations = startObservations() ;
		
		if (startedObservations>0){
			enginesStarted = true;
		}
		
		if (enginesStarted){
			out.printErr(1, "observation processes ("+startedObservations+") are running") ;
			
		}else{
			out.printErr(1, "General failure while attempting to start observations, application will stop!") ;
			System.exit(-5);
		}
		while (isActive ){
			out.delay(1000) ;
		}
	}
	
	
	@Override
	public void update( Observable oobs, Object dataObj) {
		
		out.print(4, "message received : "+((WatchedFile)dataObj).getObservation()+" ...");
		(new ObserverHandling(oobs, dataObj)).close();
		
	}

	class ObserverHandling{
		
		DocumentObserverFileCore observerCore ;
		WatchedFile wF;
		ObserverHandling oh;
		
		public ObserverHandling( Observable oobs, Object dataObj ){
		
			observerCore = (DocumentObserverFileCore)oobs;
			wF = (WatchedFile)dataObj ;
			
			observerMsgHandling.add( new WatchedFile(wF));
			
			oh = this;
		}
		
		public void close(){
			oh = null;
		}
	} // class ObserverHandling

	 
	
	
	public DocumentObserverApplicationEventIntf getAppCallbackInstance() {
		return appCallbackInstance;
	}


	public void close(){
		
		stopObservations();
		
		if (docoDataBase!=null){
			docoDataBase.disconnect() ;
		}
	}
	
	public String version(){
		return "NooLab DirectoryWatcher, "+version+"\n";
	}
	public String changeHistory(){
		changehistory = "1.0   basic functionality (NEW,MOD,DEL), single directories \n"+
						"1.02  multiple directories \n" +
						"2.0   restructuring to factory pattern, multiple directories, multithreading \n" +
						" ";
		return changehistory;
	}
	
	
	
	/**
	 * working on settings from external user-based config file : docoserv-config.xml (retrieved in   DocoControlProperties :: readSettings() )
	 * where the list of base folders or basic remote URLs is defined ...
	 * 
	 * identifying directories, establishing 1 process per directory 
	 */
	@SuppressWarnings("unused")
	protected void prepareObservations(){
		
		XmlStringHandling xMsg = new XmlStringHandling() ;
		
		ArrayList<String> folderList = new ArrayList<String>();
		ArrayList<String> insertFolder = new ArrayList<String>();
		ArrayList<String> updateFolder = new ArrayList<String>();
		 
		boolean dbOp;
		long dbKey, docid ;
		String str,basedir,pdir;
		ArrayList<String> attr,attrVals, _localSubDirs = new ArrayList<String>();
		ArrayList<ArrayList<String>> attrValSet ;
		int sub,prio,na,nv;
		DocoControlProperties dcp;
		ControlSettingsSection 	locationExcluded = null,
								locationIncluded = null,
								typesIncluded = null ;
		DocumentObserverFileCore doc;
		
		// Db db = Db.open("jdbc:h2:mem:", "sa", "sa");
		
		docoControlProperties = docoProperties.getControlProperties() ;
		dcp = docoControlProperties;
		

		
		// 1. get the list of all excluded folders
		locationExcluded = dcp.getLocationIncluded();
		typesIncluded = dcp.getTypesIncluded() ;
		
		// 2. get the explicit list of all included folders, and filter by excluded folders  
		
		locationIncluded = dcp.getLocationIncluded();
		attr = locationIncluded.getAttributes() ;
		attrValSet = locationIncluded.getAttrValues() ;
		
		na = attr.size();
		nv = attrValSet.size() ;
		
		
		
		for (int i=0;i<nv;i++){
			attrVals = attrValSet.get(i) ;
			basedir = locationIncluded.getValueOfAttribute( i, "basefolder");
			
			if (basedir.trim().length()==0){
				continue;
			}
			if (DFutils.folderExists( basedir)==false){
				String estr = "Requested folder <"+basedir+"> does not exist."; 
				// TODO: if no drive is given -> search all drives
			    out.printErr(1, estr); 
			}
			sub = xMsg.getInt( locationIncluded.getValueOfAttribute( i, "sub"),1);
			prio = xMsg.getInt( locationIncluded.getValueOfAttribute( i, "priority"),1);
			
			if (DFutils.folderExists(basedir)){
				folderList.add(basedir) ;
				if (sub>0){
					
					try{
						_localSubDirs = fileutil.listOfSubDirectories(basedir, "*", true,true);
					}catch(Exception e){
						e.printStackTrace();
					}
					
					for (int d=0;d<_localSubDirs.size();d++){
						pdir = _localSubDirs.get(d);
						pdir = StringsUtil.replaceall(pdir, "\\", "/");
						// not functional yet...
						
						out.print(4, "testing folder against exclusion patterns : "+pdir+" ...") ;
						pdir = _blockExcludedParts( pdir , sub, prio, dcp , xMsg );
						
						
						if ((pdir.length()>0) && (fileutil.direxists(pdir))){


							if (folderList.indexOf(pdir)<0){
								boolean hb=true;
								if (docoControlProperties.getFolderSelectionConstraintByExtension()>0){
									hb = docoConstraintChecker.folderConstraintExtensionSatisfied(pdir);
								}
								if (hb){
									out.print(4, "...accepted!");
									folderList.add(pdir) ;
								} // hb ?
							} // pdir not seen so far?
						} // ?
					}// d->
				} // sub ?
			} // dir exists ?
		} // i->
		
		str = "number of folders (and possibly also sub-folders) found : "+ folderList.size();
		if (folderList.size()>0){
			out.print(2, str) ;	
		}else{
			out.printErr(1, str) ;
			String prjname = this.docoControlProperties.getProjectName();
			str = "unknown" ;
			if (prjname.length()==0){
				str = "No project name has been specified so far." ;
			}

			out.printErr(1, "Please check availability and/or configuration as provided by the file: \n"+
					        "  "+ this.docoControlProperties.getSettingsFile()+" \"+" +
					        "possible reason: "+str	) ;
		}
		
		
		/* now create observer objects and 
		   put/update supervised directories into the database, using iciql over there
		   we first go through the whole list with read access, creating two lists,
		   (1) insertFolder, (2) updateFolder
		*/
		
		// opens iciDb if necessary, establishes references to the Alias-classes through reflection
		docoDataBase.checkForDbAvailability();
		DbFieldRequests accvalues;
		
		try {
			
			accvalues = new DbFieldRequests( docoDataBase.getIciDb() );
			
		} catch (Exception e) {
			e.printStackTrace();
			return;
		}
		
		if (folderList.size()>0){
			out.print(2, "updating database for found folders (n="+folderList.size()+") ...");
		}else{
			out.print(2, "no need to update database for found folders, or no folders found ...");
		}
		
		for (int i=0;i<folderList.size();i++){
			pdir = folderList.get(i);
			
			try{

				// is it listed in the db?
				dbOp = docoDataBase.checkForItem( "folders", pdir, accvalues ) ;
				
				if (dbOp==false){
					insertFolder.add( pdir );
				}else{
					updateFolder.add( pdir );
				}

			}catch(Exception e){
				e.printStackTrace();
				tuttibene = false;
			}
			
			// create an observing process...
			
			try {
				
				doc = new DocumentObserverFileCore( this, pdir, docoProcesses.size());
				docoProcesses.add(doc) ;
				
			} catch (Exception e) {
				e.printStackTrace();
			}

		} // i->
		
		out.print(2, "updating database for found folders (2) ...");
		
		for (int i=0;i<insertFolder.size();i++){
			
			pdir = insertFolder.get(i);
			pdir = StringsUtil.replaceall(pdir, "\\", "/") ;
			
			accvalues.clear();
			Integer ts = (int) (0+ System.currentTimeMillis()) ;
			accvalues.put( "changedate", ts) ;
			
			dbKey = docoDataBase.insertFolder( "folders", pdir, accvalues ) ;
			
		} // i->

		for (int i=0;i<updateFolder.size();i++){
			pdir = updateFolder.get(i);
			accvalues.clear(); 
			
			long ts = System.currentTimeMillis() ;
			accvalues.put( "changedate", ts) ;
			
			try {

				dbOp = docoDataBase.updateFolder( "folders", pdir, accvalues ) ;

			} catch (Exception e) {
				e.printStackTrace();
				tuttibene=false;
			}
		} // i->

		// TODO  remove double entries -> get script from web
		docoDataBase.removeDoubleDocumentsEntries();
		docoDataBase.orphaneVanishedDocumentEntries();
		docoDataBase.removeDoubleFolderEntries();
		
		na = 0;
		
		out.print(2, "preparing observer processes & initializing list of files ...");
		
		int maxproc = docoProcesses.size();
		
		if ((maxProcessCount>0) && (maxProcessCount < maxproc )){
			maxproc = maxProcessCount ;
		}
				
		for (int i=0;i<maxproc;i++){
										// provide % feedback
			try {
				
				doc = docoProcesses.get(i);
				boolean ok = doc.prepare("",false, dcp) ;
				
				if (ok==false){
					// msg
					String estr = "The process working on requested directory \n"+
					              "         "+doc.observedDir+"\n"+
					              "failed to start and will be removed.";
				    out.printErr(1, estr);
					docoProcesses.set(i,null) ;
					continue;
				}else{
					
					pdir = doc.getObservedDir() ;
					accvalues.clear() ;
					
					long folderid = docoDataBase.getItemKey("folders", pdir, accvalues ) ;
					
					if (folderid<=0){
						docoProcesses.set(i,null) ;
						continue;
					}
					
					docoProcesses.get(i).setFolderId(folderid);
					
					for (int d=0;d<doc.watchedFiles.size();d++){
						
						WatchedFile wF = doc.watchedFiles.get(d);
						
						wF.setFolderId(folderid);
						
						
						String filename = wF.getFilename() ;
						Long _size = wF.getFilesize();
						if (_size<=0){
							_size = DFutils.getFileSize( DFutils.createPath( wF.getHomeDir(),filename));
							wF.setFilesize(_size) ;
						}
						accvalues.clear();
						accvalues.put( "size", _size ) ;
						
						docid = docoDataBase.insertUpdateDocuments( filename, folderid, accvalues ) ;
						
					}//i->
					       

				}
				
			} catch (FileNotFoundException e) {
				e.printStackTrace();
				// msg
				docoProcesses.set(i,null) ;
				
			}
			
		} // i->

		if ((docoProcesses==null) || (docoProcesses.size()==0)){
			tuttibene=false;
		}
		String mstr ="NOT " ;
		if (tuttibene){
			mstr="";
		}
		out.print(1, "preparations have "+mstr+"been finished successfully, please wait a moment for processes being started...");
		// preparations has been finished successfully
		
		docoDataBase.removeExcludedEntries(docoControlProperties);
		
		docoDataBase.getIciDb().close();
		
		insertFolder.clear(); 
		updateFolder.clear();
		folderList.clear();
		xMsg=null;
	}
	
	



	private String _blockExcludedParts( String inDir, int sub, int prio, 
									    DocoControlProperties dcp, 
									    XmlStringHandling xMsg) {
		
		String testedDir = inDir;
		int nex ;
		ControlSettingsSection 	patternsExcluded = dcp.getPatternsExcluded();
		ControlSettingsSection 	locationExcluded = dcp.getLocationExcluded();

		ArrayList<ArrayList<String>> attrValSet = dcp.getLocationExcluded().getAttrValues() ;
		@SuppressWarnings("unused")
		ArrayList<String> attrVals ;
		String xbasedir ;
		
		nex = locationExcluded.getAttrValues().size();
		
		// int nv = attrValSet.size() ;
		
		
		
		for (int i=0;i<nex;i++){
			attrVals = attrValSet.get(i) ;
			xbasedir = locationExcluded.getValueOfAttribute( i, "basefolder");
			if ((xbasedir!=null) &&(xbasedir.length()>0)){
				if (inDir.contentEquals(xbasedir)) {
					testedDir = "";
					break;
				} else {
					// if excluded dir is sub AND prio is > included
					if (sub > 0) {

					}
				}
			}
			
		}
		
		// patterns
		
		nex = patternsExcluded.getAttrValues().size() ;
		attrValSet = dcp.getPatternsExcluded().getAttrValues() ;

		
		for (int i=0;i<nex;i++){
			attrVals = attrValSet.get(i) ;
			String _pattern = patternsExcluded.getValueOfAttribute( i, "string");
			
			String dirname = (new File(inDir)).getName() ;
			dirname = StringsUtil.replaceall(dirname, "\\", "/") ;
			
			if ((_pattern!=null) &&(_pattern.length()>0)){
				boolean hb = false;
				if (_pattern.startsWith("*")==false){
					_pattern = StringsUtil.replaceall(_pattern, "*", "") ;
					hb = dirname.startsWith(_pattern); 
				}else{
					_pattern = StringsUtil.replaceall(_pattern, "*", "") ;
					hb = dirname.contains(_pattern);
				}
				
				if (hb) {
					testedDir = ""; // we are checking excluding conditions... if one applies, then we destroy the tested string
					break;
				} else {
					// if excluded dir is sub AND prio is > included
					if (sub > 0) {

					}
				}
			}
			
		}		
		return testedDir;
	}

	/**
	 * 
	 * 
	 * 
	 * @param dir
	 * @param subdirs
	 * @return
	 * @throws Exception
	 */
	private int addWatch( String dir, boolean subdirs) throws Exception{
		String ds="";
		
		
		DocumentObserverFileCore docoWatch = new DocumentObserverFileCore(this,dir, count );
		docoWatch.prepare( dir , subdirs, docoControlProperties) ;
		
		
		docoProcesses.add(docoWatch);
		
		
		count++ ;
		observedDir.add(dir) ;

    	if (consoleMsg>0){
    		if (observationMap.size()==0){
    			ds = observationMap.size()+" directories";
    		}
    		System.out.println("Listener thread starting, observing "+ds+".");
    		if (observationMap.size()==0){
    			System.out.println("Note, that the list of directories to watch is empty.");
    		}
    		
    	}

		return count;
	}
	
	/**
	 * 
	 * returns an int ID which must be used to remove a particular watch 
	 * @param dir
	 * @param subdirs
	 * @return
	 */
	public int addListening( String dir ) throws Exception{
		return addWatch( dir, false );
	}	
	public int addListening( String dir, boolean includeSubDirs ) throws Exception{
		return addWatch( dir, includeSubDirs);
	}	

	public int removeAll(){
		int n=-1;
		
		stopObservations();
		
		while( observedDir.size()>0){
			observedDir.remove(0);
		}
		observationMap.clear();
		
		if (observedDir!=null){
			n = observedDir.size() ;
			if (consoleMsg>0){
				System.out.println("List of observed directories has not been cleared completely.");
			}			
		}
		else
		{
			n=0;
			if (consoleMsg>0){
				System.out.println("List of observed directories has been cleared completely.");
			}			
		}
		return n ;
	}
	
	public int removeListening( int _watcherID ){
		int rval=-1, _id,wpID=0,z;
		Object wpValue;
		Object removedObj = null;
		String tmp="";
		
		try{
			_id = count-1 ;
				/*
				     for (Iterator it = map.entrySet().iterator(); it.hasNext();) {
      					Map.Entry entry = (Map.Entry) it.next();
      					Object key = entry.getKey();
      					Object value = entry.getValue();
    				 }
 
				 */
				z=-1;
				rval = 2 ;
				wpID=-1;
				// 
				for (Iterator it = observationMap.keySet().iterator(); it.hasNext();) {
					z++;
      				wpValue = it.next();
      				tmp = wpValue.toString() ;
      				_id = (Integer)wpValue ;
      				if (_id==_watcherID){
      					wpID=_id;
      					
      					break;
      				}
    			}

			 
			// first we have to find the correct physical _id for the
			// requests nomological ID
				
			if (wpID>=0){
				tmp = observedDir.get(z);
				
				observedDir.remove(z);
				removedObj = observationMap.remove( _watcherID );
				
				rval=0;
				
				if (consoleMsg>0){
					System.out.println("Removed from list of observed directories "+(String)removedObj);
				}	 		

			}
			
			// System.out.println("removed entry : "+removedObj) ;
			// System.out.println("remaining map : "+observationMap.toString()) ;
			
		}catch(Exception e){
			e.printStackTrace();
		}
		return rval;
	}
	
	@Override
	public int removeListening(String guidStr) {
		// TODO Auto-generated method stub
		return 0;
	}

	public void setDelay( int millis){
		delay = millis;
	}
	@Override
	public int count() {
		// TODO Auto-generated method stub
		return 0;
	}

	@Override
	public int count(int type) {
		// TODO Auto-generated method stub
		return 0;
	}

	@Override
	public String getListening(String searchStr) {
		// TODO Auto-generated method stub
		return null;
	}

	public void setActive(boolean _isActive) {
		this.isActive = _isActive;
	}

	public int getDelay(){
		return delay;
	}
	
	public Map<Integer, DocObservation> getCurrentListenings(){
		
		return observationMap ;
	}
	
	
    public boolean isActive() {
		return isActive;
	}



	public void setApplicationMode(int applicationMode) {
		// TODO Auto-generated method stub
		
	}

	
	public void stopObservations() {
		// stop all running cores
		for ( DocumentObserverFileCore process : docoProcesses){
			if (process!=null){
				process.stop() ;
			}
		}
		
	}

	
	public int startObservations() {
		// start all observation processes for all defined folders
		int z=0;
		for ( DocumentObserverFileCore process : docoProcesses){
			if (process!=null){
				if (process.start()){
					z++;
				}
			}
		}
		return z;
	}


	public int getStartedObservations() {
		return startedObservations;
	}


	public boolean isEnginesStarted() {
		return enginesStarted;
	}
	
}

